﻿using System;
using ExcelTools;

namespace MLSharp
{
	/// <summary>
	/// In-memory representation of data that can be
	/// fed into a machine-learning process.
	/// </summary>
	public class ExcelDataSet : BasicDataSet
	{
		#region Logger

		/// <summary>
		/// The logger for the type.
		/// </summary>
		private static readonly log4net.ILog mLogger =
			log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);

		#endregion

		#region Private Fields

		/// <summary>
		/// The index of the label attribute, if any.
		/// </summary>
		private int mLabelIndex = -1;

		#endregion

		#region Private Methods

		/// <summary>
		/// Examines the specified descriptor and returns its type.
		/// </summary>
		/// <param name="descriptor">An attribute descriptor.</param>
		/// <returns>The type of the attribute.</returns>
		private static AttributeType GetAttributeType(string descriptor)
		{
			if (descriptor.Contains(","))
			{
				return AttributeType.Set;
			}
			else if (descriptor.StartsWith("discrete", StringComparison.CurrentCultureIgnoreCase))
			{
				return AttributeType.Discrete;
			}
			else if (descriptor.Equals(AttributeType.Continuous.ToString(), StringComparison.CurrentCultureIgnoreCase))
			{
				return AttributeType.Continuous;
			}
			else
			{
				throw (new InvalidOperationException("The following descriptor is invalid: " + descriptor));
			}
		}

		/// <summary>
		/// Checks to see if an extracted value represents an undefined value from a 
		/// spreadsheet.
		/// </summary>
		/// <param name="value"></param>
		/// <returns></returns>
		private static bool IsValueMissing(object value)
		{
			return value == DBNull.Value || value == null ||
				   (value is string && ((string)value) == "#NULL!") ||
				   (value is string && string.IsNullOrEmpty((string)value));
		}

		/// <summary>
		/// Loads the attribute definitions from the sheet
		/// </summary>
		/// <param name="sheet"></param>
		private void ExtractAttributes(ExcelSheet sheet)
		{
			//The first row contains the descriptions of the data items,
			//so parse and load that.
			for (int i = 0; i < sheet.Columns.Count; i++)
			{
				DataAttribute attribute;
				try
				{
					//Skip blank descriptors.
					if (sheet.Rows[0].Cells[i].Value == DBNull.Value)
						continue;

					attribute = new DataAttribute {Name = sheet.Columns[i].Name, Index = i};

					string descriptor = (string)sheet.Rows[0].Cells[i].Value;

					//If this is the label, mark it and go on.
					if (descriptor.ToUpper() == "LABEL")
					{
						mLabelIndex = i;
						continue;
					}
                    
					attribute.Type = GetAttributeType(descriptor);

					//Certain types of attributes require additional processing
					switch (attribute.Type)
					{
						case AttributeType.Discrete:
							//This descriptor should contain a number on the end
							string temp = descriptor.Remove(0, "discrete ".Length);
							attribute.DiscreteValueCount = int.Parse(temp);
							attribute.DiscreteValues = new double[0];
							break;
						case AttributeType.Set:
							//This descriptor should contain a list of values
							attribute.PossibleValues = descriptor.Split(',');
							break;
					}
				}
				catch (Exception ex)
				{
					throw (new InvalidOperationException("Error processing descriptor " + (i + 1), ex));
				}

				Attributes.Add(attribute);
			}
		}


		#endregion

		#region Public Constructors

		/// <summary>
		/// Loads the instance from the specified XLS file.
		/// </summary>
		/// <param name="dataSetFile">An XLS file.</param>
		public ExcelDataSet(string dataSetFile) : this(dataSetFile, false)
		{
			
		}

		/// <summary>
		/// Loads the instance from the specified XLS file.
		/// </summary>
		/// <param name="dataSetFile">An XLS file.</param>
		/// <param name="continueOnError">If true, errors will be logged, but an exception
		/// will not be thrown.</param>
		public ExcelDataSet(string dataSetFile, bool continueOnError)
		{
			ExcelReader reader = new ExcelReader(dataSetFile);

			ExcelSheet sheet = reader.Sheets[0];

			ExtractAttributes(sheet);

			//The remaining rows contain instances that need to be parsed
			for (int i=1; i < sheet.Rows.Count; i++)
			{
				double[] instanceValues = new double[Attributes.Count];

				for (int j = 0; j < Attributes.Count; j++)
				{
					object value = sheet.Rows[i].Cells[Attributes[j].Index].Value;

					if (IsValueMissing(value))
					{
						instanceValues[j] = double.NaN;
					}
					else
					{
						if (Attributes[j].IsNumeric())
						{
							instanceValues[j] = double.Parse((string)value);
						}
						else
						{
							string stringValue = (string) value;

							instanceValues[j] = Array.IndexOf(Attributes[j].PossibleValues, stringValue);
						}
					}
				}

				Instance instance = new Instance(instanceValues);

				//If a label is defined, grab it and stuff it in the instance.
				if (mLabelIndex >= 0)
				{
					object label = sheet.Rows[i].Cells[mLabelIndex].Value;

					if (!IsValueMissing(label))
					{
						instance.Label = (string) label;
					}
				}

				try
				{
					Instances.Add(instance);
				}
				catch (Exception ex)
				{
					string message = "Error at row " + (i + 2) + ".";

					if (continueOnError)
					{
						if (mLogger.IsWarnEnabled)
						{
							mLogger.Warn(message, ex);
						}
					}
					else
					{
						throw new InvalidOperationException("Error at row " + (i + 2) + ".", ex);
					}
				}
			}
		}

		#endregion
	}
}
